﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using System.Reflection;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography;
using System.Text;
using System.Xml.Linq;

namespace FishServe.Client.Application.Bootstrap
{
    class Program
    {
        private readonly byte[] _signingCertificateBytes = { 
            0x30, 0x82, 0x02, 0x31, 0x30, 0x82, 0x01, 0x9A, 0xA0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x10, 0xD6, 0xDE, 0xAC, 0x6F, 0xAF, 
            0x63, 0x3B, 0xA0, 0x4F, 0x18, 0xC6, 0x2C, 0x19, 0xE8, 0x84, 0xD8, 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 
            0x0D, 0x01, 0x01, 0x05, 0x05, 0x00, 0x30, 0x57, 0x31, 0x55, 0x30, 0x53, 0x06, 0x03, 0x55, 0x04, 0x03, 0x1E, 0x4C, 0x00, 
            0x7B, 0x00, 0x39, 0x00, 0x36, 0x00, 0x41, 0x00, 0x38, 0x00, 0x33, 0x00, 0x34, 0x00, 0x43, 0x00, 0x32, 0x00, 0x2D, 0x00, 
            0x38, 0x00, 0x34, 0x00, 0x38, 0x00, 0x38, 0x00, 0x2D, 0x00, 0x34, 0x00, 0x46, 0x00, 0x43, 0x00, 0x45, 0x00, 0x2D, 0x00, 
            0x38, 0x00, 0x30, 0x00, 0x42, 0x00, 0x32, 0x00, 0x2D, 0x00, 0x38, 0x00, 0x44, 0x00, 0x45, 0x00, 0x38, 0x00, 0x43, 0x00, 
            0x33, 0x00, 0x42, 0x00, 0x44, 0x00, 0x46, 0x00, 0x35, 0x00, 0x42, 0x00, 0x37, 0x00, 0x7D, 0x30, 0x1E, 0x17, 0x0D, 0x30, 
            0x38, 0x31, 0x32, 0x32, 0x38, 0x30, 0x38, 0x34, 0x37, 0x34, 0x39, 0x5A, 0x17, 0x0D, 0x30, 0x39, 0x31, 0x32, 0x32, 0x38, 
            0x31, 0x34, 0x34, 0x37, 0x34, 0x39, 0x5A, 0x30, 0x57, 0x31, 0x55, 0x30, 0x53, 0x06, 0x03, 0x55, 0x04, 0x03, 0x1E, 0x4C, 
            0x00, 0x7B, 0x00, 0x39, 0x00, 0x36, 0x00, 0x41, 0x00, 0x38, 0x00, 0x33, 0x00, 0x34, 0x00, 0x43, 0x00, 0x32, 0x00, 0x2D, 
            0x00, 0x38, 0x00, 0x34, 0x00, 0x38, 0x00, 0x38, 0x00, 0x2D, 0x00, 0x34, 0x00, 0x46, 0x00, 0x43, 0x00, 0x45, 0x00, 0x2D, 
            0x00, 0x38, 0x00, 0x30, 0x00, 0x42, 0x00, 0x32, 0x00, 0x2D, 0x00, 0x38, 0x00, 0x44, 0x00, 0x45, 0x00, 0x38, 0x00, 0x43, 
            0x00, 0x33, 0x00, 0x42, 0x00, 0x44, 0x00, 0x46, 0x00, 0x35, 0x00, 0x42, 0x00, 0x37, 0x00, 0x7D, 0x30, 0x81, 0x9F, 0x30, 
            0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x81, 0x8D, 0x00, 0x30, 0x81, 
            0x89, 0x02, 0x81, 0x81, 0x00, 0xBF, 0x4F, 0x44, 0xF0, 0x6D, 0x6E, 0x7D, 0xF9, 0x6E, 0x1E, 0x76, 0x5B, 0xA9, 0x18, 0x98, 
            0xCD, 0x8A, 0xA1, 0x13, 0x8C, 0x25, 0xAB, 0xC1, 0xAD, 0x24, 0x31, 0xD5, 0x4A, 0xAD, 0x65, 0x5B, 0x42, 0x7F, 0x2E, 0x6B, 
            0x4A, 0xE0, 0x12, 0x83, 0xE7, 0x27, 0xD1, 0x20, 0x1D, 0x8F, 0x8F, 0x1E, 0xC1, 0xFB, 0x16, 0xFD, 0x19, 0x42, 0xBB, 0xE7, 
            0xAD, 0x2F, 0xAE, 0x05, 0xC9, 0x93, 0xF5, 0xE3, 0x5B, 0xDE, 0x3C, 0x73, 0x52, 0x8C, 0x5A, 0x42, 0xDC, 0xD8, 0x0C, 0x4A, 
            0xE5, 0x18, 0xA0, 0xFD, 0x61, 0x30, 0x87, 0x53, 0xEB, 0x6A, 0xF3, 0x18, 0xCC, 0x29, 0x7C, 0x23, 0xCD, 0x2E, 0x5C, 0xD5, 
            0x09, 0xB4, 0x71, 0x39, 0x34, 0x15, 0xA2, 0x16, 0x02, 0x0D, 0xEE, 0x64, 0x6A, 0x14, 0x71, 0xBF, 0xC3, 0xC8, 0x3B, 0xB6, 
            0x0F, 0x48, 0x8C, 0x73, 0x3E, 0xBC, 0xEC, 0xA3, 0xCD, 0x7D, 0x68, 0xBE, 0x1D, 0x02, 0x03, 0x01, 0x00, 0x01, 0x30, 0x0D, 
            0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x05, 0x05, 0x00, 0x03, 0x81, 0x81, 0x00, 0x31, 0xE8, 0x0F, 
            0xF2, 0x05, 0x55, 0x89, 0x18, 0xDB, 0x59, 0x57, 0x8D, 0x48, 0x0D, 0x26, 0x35, 0xD5, 0x71, 0xEC, 0x8D, 0xE4, 0x13, 0xBB, 
            0x5F, 0xF1, 0x6C, 0x0F, 0x98, 0xE9, 0x72, 0xE1, 0x71, 0x88, 0xB3, 0x59, 0x5B, 0xB0, 0x6D, 0x58, 0xD6, 0xEA, 0x3F, 0x29, 
            0xA3, 0x0D, 0x0C, 0xCB, 0x99, 0x6E, 0x7C, 0x34, 0xD6, 0x2F, 0xC5, 0xA0, 0xC3, 0x9D, 0x2F, 0x56, 0x9A, 0xC5, 0x66, 0xAD, 
            0x63, 0xAB, 0xD8, 0xC1, 0x8B, 0xB6, 0x84, 0xCD, 0xEA, 0xEA, 0x46, 0xD6, 0x2D, 0x5F, 0x9C, 0x24, 0x89, 0xD1, 0xCC, 0xE4, 
            0x8F, 0xBB, 0xEA, 0x98, 0xA1, 0xDC, 0x76, 0x84, 0x2A, 0xC5, 0xBE, 0xE3, 0x78, 0xBB, 0x81, 0x59, 0x9D, 0x18, 0x97, 0x77, 
            0x91, 0x8F, 0x03, 0x38, 0x50, 0x31, 0x23, 0x95, 0xDF, 0x69, 0x8C, 0xDF, 0x16, 0x6B, 0x52, 0x6E, 0x47, 0x29, 0x6B, 0x11, 
            0xD6, 0x61, 0x90, 0x03, 0x51 };

        private readonly string _modulesFolderName = "Modules";
 
        #region Preparation

        // Create certificate.cer from .pfx
        //X509Certificate2 cert = new X509Certificate2(@"..\FishServe\Solution Items\FishServe.CodeSigning.pfx", "password");
        //byte[] exportCertBytes = cert.Export(X509ContentType.Cert);
        //File.WriteAllBytes(@"..\FishServe\Solution Items\FishServe.CodeSigning.cer", exportCertBytes);

        // Get raw certificate data
        //X509Certificate certificate = X509Certificate.CreateFromCertFile(@"..\FishServe\Solution Items\FishServe.CodeSigning.cer");
        //byte[] rawCertBytes = certificate.GetRawCertData();
        //string hexByteArray = "";
        //for (int i = 0; i < rawCertBytes.Length; i++)
        //{
        //    hexByteArray += string.Format("0x{0:X2}", rawCertBytes[i]);
        //    if (i < rawCertBytes.Length - 1)
        //        hexByteArray += ", ";
        //}
        //string certificateField = "private const byte[] signingCertificateBytes = { " + hexByteArray + " };";

        // Sign/Verify example using pfx (includes public and private key)
        //X509Certificate2 cert = new X509Certificate2(@"..\FishServe\Solution Items\FishServe.CodeSigning.pfx", "password");
        //RSACryptoServiceProvider csp = cert.PrivateKey as RSACryptoServiceProvider;
        //string dataToSign = "abcdefghijklmnopqrstuvwxyz1234567890";
        //byte[] dataToSignBytes = UTF8Encoding.UTF8.GetBytes(dataToSign);
        //byte[] signatureBytes = csp.SignData(dataToSignBytes, new SHA1CryptoServiceProvider());
        //string encodedSignatureBytes = Convert.ToBase64String(signatureBytes);
        //bool verify = csp.VerifyData(dataToSignBytes, new SHA1CryptoServiceProvider(), signatureBytes);

        // Verify example using embedded certificate (includes public key only)
        //X509Certificate2 cert2 = new X509Certificate2(_signingCertificateBytes);
        //RSACryptoServiceProvider csp2 = cert2.PublicKey.Key as RSACryptoServiceProvider;
        //bool verify2 = csp2.VerifyData(dataToSignBytes, new SHA1CryptoServiceProvider(), signatureBytes);

        #endregion

        [STAThread]
        static void Main()
        {
            new Program().Run();
        }

        private bool Run()
        {
            string applicationDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);

            string[] manifestFilenames = Directory.GetFiles(applicationDir, "*.manifest", SearchOption.TopDirectoryOnly);

            if (manifestFilenames.Length == 0)
            {
                MessageBox.Show(String.Format("Application startup failed.\r\n\r\nNo manifest files in\r\n'{0}'", applicationDir), "FishServe Application Startup", MessageBoxButtons.OK, MessageBoxIcon.Stop);
                return false;
            }

            //if (manifestFilenames.Length > 1)
            //    Array.Sort<string>(manifestFilenames);

            X509Certificate2 certificate = new X509Certificate2(_signingCertificateBytes);
            RSACryptoServiceProvider publicKeyCsp = certificate.PublicKey.Key as RSACryptoServiceProvider;

            string latestReleaseManifestFilename = "";
            DateTime latestReleaseManifestCreated = DateTime.MinValue;
            XDocument latestSignedReleaseManifestDoc = null;
            XDocument latestReleaseManifestDoc = null;
            XElement encodedReleaseSignatureElement = null;
            XElement encodedReleaseManifestElement = null;
            byte[] releaseManifestBytes;
            byte[] releaseSignatureBytes;

            foreach (string manifestFilename in manifestFilenames)
            {
                if (manifestFilename.EndsWith("vshost.exe.manifest"))
                    continue;

                XDocument manifestDoc = null;
                // TODO: Use XmlTextReader to avoid loading the entire document.
                try { manifestDoc = XDocument.Load(manifestFilename); }
                catch(Exception) { continue; }

                XElement signedReleaseManifestElement = manifestDoc.Element("SignedReleaseManifest");
                if (signedReleaseManifestElement == null)
                    continue;

                try
                {
                    encodedReleaseManifestElement = signedReleaseManifestElement.Element("EncodedManifest");
                    releaseManifestBytes = Convert.FromBase64String(encodedReleaseManifestElement.Value);
                    string releaseManifestString = UTF8Encoding.UTF8.GetString(releaseManifestBytes);
                    // TODO: Use XmlTextReader to avoid loading the entire document.
                    XDocument releaseManifestDoc = XDocument.Parse(releaseManifestString);

                    string releaseManifestType = releaseManifestDoc.Element("ReleaseManifest").Attribute("Type").Value;
                    if (releaseManifestType.ToLower() == "solution")
                    {
                        DateTime releaseManifestCreated = DateTime.ParseExact(releaseManifestDoc.Element("ReleaseManifest").Attribute("Created").Value, "yyyy-MM-dd HHHH:mm:ss", null);
                        if (releaseManifestCreated > latestReleaseManifestCreated)
                        {
                            latestReleaseManifestCreated = releaseManifestCreated;
                            latestReleaseManifestFilename = manifestFilename;
                            latestSignedReleaseManifestDoc = manifestDoc;
                            latestReleaseManifestDoc = releaseManifestDoc;
                        }
                    }
                }
                catch (Exception) 
                {
                    continue;
                }
            }

            if (latestSignedReleaseManifestDoc == null)
            {
                MessageBox.Show(String.Format("Application startup failed.\r\n\r\nCould not find a valid application manifest in '{0}'.", applicationDir), "FishServe Application Startup", MessageBoxButtons.OK, MessageBoxIcon.Stop);
                return false;
            }

            encodedReleaseSignatureElement = latestSignedReleaseManifestDoc.Element("SignedReleaseManifest").Element("EncodedSignature");
            encodedReleaseManifestElement = latestSignedReleaseManifestDoc.Element("SignedReleaseManifest").Element("EncodedManifest");
            releaseManifestBytes = Convert.FromBase64String(encodedReleaseManifestElement.Value);
            releaseSignatureBytes = Convert.FromBase64String(encodedReleaseSignatureElement.Value);

            if (!publicKeyCsp.VerifyData(releaseManifestBytes, new SHA1CryptoServiceProvider(), releaseSignatureBytes))
            {
                MessageBox.Show(String.Format("Application startup failed.\r\n\r\nCould not verify signature in\r\n'{0}'", latestReleaseManifestFilename) , "FishServe Application Startup", MessageBoxButtons.OK, MessageBoxIcon.Stop);
                return false;
            }

            // MessageBox.Show(String.Format("Loading release manifest (application) {0}", Path.GetFileName(latestManifestFilename)), "FishServe Application Startup", MessageBoxButtons.OK, MessageBoxIcon.Information);

            // Find the first application module manifest
            XElement moduleManifestsElement = latestReleaseManifestDoc.Element("ReleaseManifest").Element("ModuleManifests");
            if (moduleManifestsElement == null)
            {
                MessageBox.Show(String.Format("Application startup failed.\r\n\r\nModuleManifests element not found in '{0}'.", latestReleaseManifestFilename), "FishServe Application Startup", MessageBoxButtons.OK, MessageBoxIcon.Stop);
                return false;
            }

            XElement[] moduleManifests = moduleManifestsElement.Elements("ModuleManifest").ToArray<XElement>();
            if (moduleManifests == null || moduleManifests.Length == 0)
            {
                MessageBox.Show(String.Format("Application startup failed.\r\n\r\nModuleManifest element(s) not found in ModuleManifests element in '{0}'.", latestReleaseManifestFilename), "FishServe Application Startup", MessageBoxButtons.OK, MessageBoxIcon.Stop);
                return false;
            }

            foreach (XElement moduleManifestElement in moduleManifestsElement.Elements())
            {
                if (moduleManifestElement.Name != "ModuleManifest")
                {
                    MessageBox.Show(String.Format("Application startup failed.\r\n\r\nUnexpected element '{0}' in ModuleManifests element in '{1}'.", moduleManifestElement.Name, latestReleaseManifestFilename), "FishServe Application Startup", MessageBoxButtons.OK, MessageBoxIcon.Stop);
                    return false;
                }

                XAttribute typeAttribute = moduleManifestElement.Attribute("ManifestType");
                if (typeAttribute == null || String.IsNullOrEmpty( typeAttribute.Value ))
                {
                    MessageBox.Show(String.Format("Application startup failed.\r\n\r\nMissing ManifestType attribute in ModuleManifest element in '{0}'.", latestReleaseManifestFilename), "FishServe Application Startup", MessageBoxButtons.OK, MessageBoxIcon.Stop);
                    return false;
                }

                if (typeAttribute.Value.ToLower() == "application")
                {
                    return LoadApplicationModule(moduleManifestElement, applicationDir, latestReleaseManifestDoc, latestReleaseManifestFilename);
                }
            }

            // TODO: Cleanup/Dispose

            return false;
        }

        private bool LoadApplicationModule(XElement moduleManifestElement, string applicationDir, XDocument releaseManifestDoc, string releaseManifestFilename)
        {
            XElement moduleElement = moduleManifestElement.Element("Module");
            if (moduleElement == null)
            {
                MessageBox.Show(String.Format("Application startup failed.\r\n\r\nModule element missing from (application) ModuleManifest element in '{0}'.", releaseManifestFilename), "FishServe Application Startup", MessageBoxButtons.OK, MessageBoxIcon.Stop);
                return false;
            }

            XAttribute moduleNameAttribute = moduleElement.Attribute("ModuleName");
            if (moduleNameAttribute == null || String.IsNullOrEmpty(moduleNameAttribute.Value))
            {
                MessageBox.Show(String.Format("Application startup failed.\r\n\r\nName attribute missing from Module element in (application) ModuleManifest element in '{0}'.", releaseManifestFilename), "FishServe Application Startup", MessageBoxButtons.OK, MessageBoxIcon.Stop);
                return false;
            }

            XAttribute moduleVersionAttribute = moduleElement.Attribute("Version");
            if (moduleVersionAttribute == null || String.IsNullOrEmpty(moduleVersionAttribute.Value))
            {
                MessageBox.Show(String.Format("Application startup failed.\r\n\r\nVersion attribute missing from Module element in (application) ModuleManifest element in '{0}'.", releaseManifestFilename), "FishServe Application Startup", MessageBoxButtons.OK, MessageBoxIcon.Stop);
                return false;
            }

            string applicationModuleName = moduleNameAttribute.Value;
            string applicationModuleVersion = moduleVersionAttribute.Value;

            // TODO: create new application domain
            AssemblyResolver.Initialise(applicationDir, _modulesFolderName);

            // The following is no longer neeeded.  AssemblyResolver will take care of loading dependent modules.
            // Update: there may be a performance reason to load dependent modules early (expecially for the 
            // the Application module) rather than waiting for the CLR to exhaust it's binding process then referring to
            // AssemblyResolver.  This would also reduce risk that the CLR binds to the wrong assembly version (i.e. transient 
            // assemblies in the wrong directory).  Having assemblies strongly named should avoid the CLR
            // bindng to the wrong assembly version.
            //
            //XElement dependentModulesElement = latestManifestDoc.Element("Manifest").Element("DependentModules");
            //if (dependentModulesElement != null)
            //    if (!LoadDependentModules(dependentModulesElement, applicationDir))
            //        return false;

            string applicationModuleFullFilename = Path.Combine(Path.Combine(Path.Combine(Path.Combine(applicationDir, _modulesFolderName), applicationModuleName), applicationModuleVersion), applicationModuleName + ".dll");
            if (!File.Exists(applicationModuleFullFilename))
            {
                MessageBox.Show(String.Format("Application startup failed.\r\n\r\nApplication module assembly not found '{0}'.", applicationModuleFullFilename), "FishServe Application Startup", MessageBoxButtons.OK, MessageBoxIcon.Stop);
                return false;
            }

            Assembly applicationAssembly = null;
            // Note, all assemblies will need to be loaded into the LoadFrom context.
            try
            {
                applicationAssembly = Assembly.LoadFrom(applicationModuleFullFilename);
            }
            catch (Exception)
            {
                MessageBox.Show(String.Format("Application startup failed.\r\n\r\nFailed to load application module '{0}'.", applicationModuleFullFilename), "FishServe Application Startup", MessageBoxButtons.OK, MessageBoxIcon.Stop);
                return false;
            }

            // TODO: rethink blocking

            Type[] types = applicationAssembly.GetTypes();
            bool found = false;
            foreach (Type type in types)
            {
                if (type.Name == "ApplicationActivator")
                {
                    found = true;
                    // ApplicationActivator.Start() takes two parameters:  string applicationDir, string releaseManifestFilename
                    MethodInfo startMethodInfo = type.GetMethod("Start", new Type[] { typeof( string ), typeof( string ) } );
                    if( startMethodInfo == null )
                    {
                        MessageBox.Show (String.Format( "ApplicationActivator.Start() method not found in '{0}' ", applicationModuleFullFilename), "FishServe Application Startup", MessageBoxButtons.OK, MessageBoxIcon.Stop);
                        return false;
                    }

                    object applicationActivator = Activator.CreateInstance(type);

                    object methodReturn = null;
                    try
                    {
                        methodReturn = startMethodInfo.Invoke(applicationActivator, new object[] { applicationDir, releaseManifestFilename });
                    }
                    catch (Exception ex)
                    {
                        // TODO: 
                        throw new Exception(String.Format("Failed to invoke ApplicationActivator.Start() in '{0}'", applicationModuleFullFilename), ex);
                    }

                    return (bool)methodReturn;
                }
            }

            if (!found)
            {
                MessageBox.Show(String.Format("Application startup failed.\r\n\r\nApplicationActivator type not found in '{0}'.", applicationModuleFullFilename), "FishServe Application Startup", MessageBoxButtons.OK, MessageBoxIcon.Stop);
                return false;
            }

            return true;
        }

        private bool LoadDependentModules(XElement dependentModulesElement, string applicationDir)
        {
            // An Application module should have very limited dependencies on other modules (ideally only FishServe.Core).  
            // The objective of the Application module is to load key services (e.g. ReleaseManifestReader and ModuleLoader services)
            // required to continue to the next step which is to load the Shell module. 
            // If the dependent module(s) loaded below contain further module dependencies then the Application module
            // should fail.
            // Update: the AssemblyResolver will take care of all dependencies
            foreach (XElement dependentModule in dependentModulesElement.Elements())
            {
                string moduleName = dependentModule.Attribute("ModuleName").Value;
                string moduleVersion = dependentModule.Attribute("Version").Value;
                string moduleGuid = dependentModule.Attribute("ModuleGuid").Value;
                string manifestGuid = dependentModule.Attribute("ManifestGuid").Value;
                // TODO: load manifest of dependent module and confirm the manifest guid is correct

                string moduleVersionDir = Path.Combine( Path.Combine(Path.Combine(applicationDir, _modulesFolderName), moduleName), moduleVersion );
                if (!Directory.Exists(moduleVersionDir))
                {
                    string msg = String.Format( "Application startup failed.  Dependent module '{0}' v{1} could not be loaded.  Directory not found: {2}", moduleName, moduleVersion, moduleVersionDir );
                    MessageBox.Show(msg, "FishServe Application Startup", MessageBoxButtons.OK, MessageBoxIcon.Stop);
                    return false;
                }
                
                string moduleFullFilename = Path.Combine(moduleVersionDir, moduleName + ".dll");
                try
                {
                    Assembly.LoadFrom(moduleFullFilename);
                    MessageBox.Show("Loaded dependent module " + moduleFullFilename, "FishServe.Client.Application.Bootstrap.exe", MessageBoxButtons.OK, MessageBoxIcon.Information);
                }
                catch( Exception )
                {
                    string msg = String.Format("Application startup failed.  Dependent module '{0}' v{1} could not be loaded.  Failed to load assembly {2}", moduleName, moduleVersion, moduleFullFilename);
                    MessageBox.Show(msg, "FishServe Application Startup", MessageBoxButtons.OK, MessageBoxIcon.Stop);
                    return false;
                }
            }
            return true;
        }
    }
}
